package com.transfiguro.utilities;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.transfiguro.exceptions.ConversionFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * This class provides methods to parse a given mapping XML file, determine
 * if a specific type requires conversion or not, get the converted value for
 * a given string
 */
public class MappingXMLParser {
    private final Logger logger = LoggerFactory.getLogger(MappingXMLParser.class);
    private Document document;
    private File xmlFile;
    private static final String CONVERT_TAG = "convert";
    private static Multimap<String, Map<String, String>> fromToMap;

    /**
     * Constructor which takes in the XML file to be used
     *
     * @param file
     */
    public MappingXMLParser(final File file) {
        xmlFile = file;
        fromToMap = ArrayListMultimap.create();
        init();
        initMap();
    }

    /**
     * Initializer for the <code>document</code>
     */
    private void init() {
        try {
            getDocument();
        } catch (final ConversionFailedException e) {
            logger.debug("Exception: {}", e.getMessage());
        }

    }

    /**
     * Initializer for the <code>fromToMap</code>
     */
    private void initMap() {
        final NodeList nodeList = document.getElementsByTagName("convert");
        for (int index = 0; index < nodeList.getLength(); index++) {
            final Node node = nodeList.item(index);
            final Element element = (Element) node;
            final String type = ((Element) node).getAttribute("type");
            final Map<String, String> valueMap = new HashMap<String, String>();
            valueMap.put(getChildValue("from", element), getChildValue("to", element));
            fromToMap.put(type, valueMap);
        }

    }

    /**
     * Given an element and a tag for the child, this method returns the
     * node value of the child
     *
     * @param tag      The tag for the child
     * @param element  The parent element
     * @return  The value of the child node
     */
    private String getChildValue(final String tag, final Element element) {
        final NodeList list = element.getElementsByTagName(tag).item(0).getChildNodes();
        final Node node = (Node) list.item(0);

        return node.getNodeValue();
    }

    /**
     * This method parses the XML file and creates the <code>document</code>
     *
     * @throws ConversionFailedException  if there is an error in parsing
     */
    private void getDocument() throws ConversionFailedException {
        try {
            final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            final DocumentBuilder dBuilder = documentBuilderFactory.newDocumentBuilder();
            document = dBuilder.parse(xmlFile);
        } catch (final ParserConfigurationException e) {
            throw new ConversionFailedException(e.getMessage());
        } catch (final SAXException e) {
            throw new ConversionFailedException(e.getMessage());
        } catch (final IOException e) {
            throw new ConversionFailedException(e.getMessage());
        }
    }

    /**
     * For a given type and a name, this method tells whether its value
     * requires conversion or not
     *
     * @param type  The type of the value
     * @param name  The value for which conversion requirement has to be determined
     * @return  <tt>True</tt> if conversion is required
     *         <tt>False</tt> if conversion is not required
     */
    public boolean requiresConversion(final String type, final String name) {
        boolean result = false;
        final Collection<Map<String, String>> convertList = fromToMap.get(type);
        for (final Map<String, String> child : convertList) {
            if (child.containsKey(name)) {
                result = true;
            }
        }
        return result;
    }

    /**
     * Given the type of value and the value from, this method returns
     * the converted value
     *
     * @param type  The type of the value
     * @param from  The value itself
     * @return  The converted value
     */
    public String getTo(final String type, final String from) {
        String result = "";
        final Collection<Map<String, String>> convertList = fromToMap.get(type);
        for (final Map<String, String> child : convertList) {
            if (child.containsKey(from)) {
                result = child.get(from);
            }
        }
        return result;
    }

}
